%% records.erl
%%      Extracts the record definitions from an erlang module's AST into a more 
%%      compact form, and provides methods to query basic record parametrs
%%      from that simplified data structure.
%% 
%% Copyright (C) 2008, 2009 Gábor Fehér
%%
%% This file is part of ECT.
%% 
%% ECT is free software: you can redistribute it and/or modify
%% it under the terms of the GNU General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%% 
%% ECT is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%% GNU General Public License for more details.
%% 
%% You should have received a copy of the GNU General Public License
%% along with ECT.  If not, see <http://www.gnu.org/licenses/>.
%%
-module(records).
-export([scan/1, lookup/2, fieldpos/2, form_to_record/1, field_accessor/4]).

%% Converts an AST record description into a simpler format.
%% Input: the AST form of the record
%% Returns: {RecordName, [FieldName1, FieldName2, FieldName3, ...]}
%% Note that default(=initial) field values are dropped.
form_to_record({attribute,_,record,{Name,List}}) ->
    {Name,
     lists:map(
        fun({record_field,_,{atom,_,FieldName}}) ->
            FieldName;
           ({record_field,_,{atom,_,FieldName},_}) ->
            FieldName
        end,
        List
     )
    }.

%% Extracts the record definitions from an erlang programs full AST.
%% Input: erlang program AST
%% Returns: list of records, in the form of {RecordName, [FieldName1, FieldName2, ...]}
%% Note that the default field values are dropped.
scan(Forms) ->
    R1 = lists:filter(
	   fun(F) -> 
		   case F of
		       {attribute,_,record,_} -> true;
		       _ -> false
		   end
	   end,
	   Forms
	  ),
    Records = lists:map(
		fun(F) -> form_to_record(F) end,
		R1),
    Records.

%% Looks up a records definition in the simplified record definition list. (see scan)
%% Input: definition list, record name
%% Returns: simplified record definition (see form_to_record)
lookup([{Name, List}|_], Name) ->
    {record, Name, List};
lookup([_|List], Name) ->
    lookup(List, Name);
lookup([], _) ->
	notfound.

%% Calculates the position of a named field in the tuple of a record.
%% Input: record definition, field name
%% Returns: {ok, Pos} If the named field (Field) exists in the record definition (Fields). 
%%                    Here Pos is the position, which can be used with setelement and element
%%                    to access the field.
%%          {error, Message} If the nemad field does not exist in the record definition.
fieldpos({record, RecName, Fields}, Field) ->
    % start couning at 2, since the first element in a record's tuple
    % is the record name
    fieldpos(Fields, Field, 2, RecName).
fieldpos([], Field, _, RecName) ->
    {error, io_lib:format("[ect] field ~p undefined in record ~p", [Field, RecName])};
fieldpos([Field|_], Field, N, _) ->
    {ok, N};
fieldpos([_|Fields], Field, N, RecName) ->
    fieldpos(Fields, Field, N+1, RecName).

%% Function: field_accessor(Definition, FieldName, RecExpr, Line)
%% Purpose: generates a simple AST fragment, that gets a given field of an object
%% Arguments:
%%   Definition: definition of record, as returned by lookup
%%   FieldName: name of field to get
%%   RecExpr: the record, whose field the expression will get
%%   Line: line number to store in AST nodes of the expression
%% Returns:
%%   The AST equivalent of the following source code fragment:
%%   element(NNN, RecExpr) where NNN is calculated using Definition and FieldName
field_accessor(Definition, FieldName, RecExpr, Line) ->
	case fieldpos(Definition, FieldName) of
	{ok, FieldPos} ->
		ast:element(FieldPos, RecExpr, Line);
	{error, ErrMsg} ->
        throw({translateError, Line, ErrMsg})
	end.
